Appearance
側邊欄功能實現
實現概念說明
- 畫面呈現上會有主選單列"HeaderMenu"、頁面內容區"Content"、側邊欄頁面切換"SplitView.Pane"、底部資訊欄"Footer"
- 使用SplitView這元件來實現側邊欄效果,這元件主要分為"<SplitView.Pane>"、"<SplitView.Content>"分別負責側邊欄顯示與頁面內容顯示
- <SplitView.Pane> 欄位可以由各種元件組成,這邊採用的是 ListBox來實現選單按鍵
- 此篇文章聚焦在"基礎版型實現"、"側邊欄開關"、"頁面切換"功能實現
此篇當作筆記看看就好,紀錄到後面自己也有些亂掉了,未來有機會時再找機會重新整理
一、基礎版型實現
二、HeaderMenu
編輯 Views 目錄下的 MainWindow.axaml,這是 MVVM 樣板預設的主視窗。我們首先定義一個 Grid 作為整體的佈局框架,並設定 RowDefinitions="Auto, *" 來規劃垂直空間。這代表將視窗切分為上下兩列:第一列的高度設為 Auto(隨內容自動調整)=>HeaderMenu高度,而第二列的高度則設為 *(自動填滿視窗剩餘的所有空間)=>側邊欄高度。
xml
<!-- 主視窗框架區塊 -->
<Grid RowDefinitions="Auto, *">
</Grid>
完成主視窗的網格佈局 (Grid Layout) 設定後,我們就能開始填入 UI 元件。首先,在畫面上方加入一個 Menu 作為功能列。
這裡有兩個關鍵設定:首先透過 Grid.Row="0" 將HeaderMenu定位於我們剛剛定義的第一列(即高度為 Auto 的區域),接著設定 Background 屬性來定義HeaderMenu的背景色。而在 Menu 內部,我們使用 MenuItem 來建立具體的選單選項,並透過 Header 屬性來設定顯示的文字或符號(例如漢堡圖示 "☰")。
xml
<!-- 主視窗框架區塊 -->
<Grid RowDefinitions="Auto, *">
<!-- Header Menu-->
<Menu Background="#ECF0F1" Grid.Row="0">
<MenuItem Header="☰"/>
<MenuItem Header="關於">
<MenuItem Header="關於應用" />
</MenuItem>
</Menu>
</Grid>
三、實作側邊欄 (Sidebar) 的顯示與隱藏
這邊主要分為 View 與 ViewModel 兩部分的整合:
首先在 View (MainWindow) 加入 SplitView 控制項。我們設定 Grid.Row="1" 讓側邊欄顯示於 Header Menu 下方,並將 DisplayMode 設為 "Overlay" (覆蓋模式),OpenPaneLength 設定為 200 以定義展開寬度。
最關鍵的屬性是 IsPaneOpen,我們將其雙向綁定 (TwoWay) 至 ViewModel 的 IsSidebarOpen 屬性:
xml
IsPaneOpen="{Binding IsSidebarOpen, Mode=TwoWay}"
如此一來,側邊欄的顯示狀態就完全由變數決定。
接著在 ViewModel (MainWindowViewModel.cs) 中建立 _isSidebarOpen 變數,並實作 ToggleSidebar 邏輯與 ToggleSidebarCommand。
最後,將介面上的漢堡選單按鈕 (MenuItem Header="☰") 綁定至 ToggleSidebarCommand。每當按鈕被點擊,Command 便會翻轉 IsSidebarOpen 的布林值,進而驅動 UI 的 IsPaneOpen 屬性,完成側邊欄的開關切換。
xml
MainWindow.axml
<!-- 主視窗框架區塊 -->
<Grid RowDefinitions="Auto, *">
<!-- Header Menu-->
<Menu Background="#ECF0F1" Grid.Row="0">
<MenuItem Header="☰" Command="{Binding ToggleSidebarCommand}"/>
<MenuItem Header="關於">
<MenuItem Header="關於應用" />
</MenuItem>
</Menu>
<!--側邊欄-->
<SplitView Grid.Row="1"
DisplayMode="Overlay"
OpenPaneLength="200"
IsPaneOpen="{Binding IsSidebarOpen, Mode=TwoWay}">
<SplitView.Pane>
<Border BorderBrush="#BDC3C7"
BorderThickness="0, 0, 1, 0">
<StackPanel Background="#ECF0F1">
<TextBlock Text="導航選單"
FontSize="16"
HorizontalAlignment="Center"
Margin="15, 20, 15, 10"/>
</StackPanel>
</Border>
</SplitView.Pane>
</SplitView>
</Grid>
在 ViewModel 的實作部分,我們使用 MVVM Toolkit 來簡化程式碼。透過 [ObservableProperty] 標籤修飾私有變數,套件便會自動產生可供 View 綁定的公開屬性與通知機制;而在函數上方加上 [RelayCommand] 標籤後,系統則會自動生成對應的 Command(例如 ToggleSidebar 會生成 ToggleSidebarCommand),讓 View 端的元件可以直接呼叫使用。
C#
MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace AvaloniaSideBarEx.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private bool _isSidebarOpen;
[RelayCommand]
private void ToggleSidebar()
{
IsSidebarOpen = !IsSidebarOpen;
}
}
四、實作側邊欄 (Sidebar) 的頁面切換
頁面準備 View/ViewModel
首先在Views增加3個測試用的頁面(UserControl),AboutView、HomeView、SettingsView。
除了新增View之外還需要先建立好View所對應的ViewModel,並先簡單建立歡迎訊息
ViewModel修改時除了要寫變數之外還要記得繼承 ViewModelBase ,這樣後續才可存入ObservableObject
讓View與ViewModel來綁定,其中的關鍵就是
xmlns:vm="clr-namespace:AvaloniaSideBarEx.ViewModels"、x:DataType="vm:HomeViewModel",如此一來在View就可以使用ViewModel了
xml
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:AvaloniaSideBarEx.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:HomeViewModel"
x:Class="AvaloniaSideBarEx.Views.HomeView">
<StackPanel Margin="20" Spacing="15">
<TextBlock Text="{Binding WelcomeMessage}"
FontSize="16"
Foreground="#34495e"/>
</StackPanel>
</UserControl>
建立DI容器提供服務
- NuGet來安裝DI套件 Microsoft.Extensions.DependencyInjection
- 修改App.axaml.cs (定義 ServiceProvider、初始化 DI 容器、建立 Provider、解析 MainWindow 並注入 ViewModel)
C#
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using System.Linq;
using Avalonia.Markup.Xaml;
using AvaloniaSideBarEx.ViewModels;
using AvaloniaSideBarEx.Views;
using Microsoft.Extensions.DependencyInjection;
namespace AvaloniaSideBarEx;
public partial class App : Application
{
// 定義 ServiceProvider
public IServiceProvider? Services { get; set; }
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
// 初始化 DI 容器
var collection = new ServiceCollection();
// 註冊 ViewModels
collection.AddSingleton<MainWindowViewModel>();
collection.AddSingleton<HomeViewModel>();
collection.AddSingleton<SettingsViewModel>();
collection.AddSingleton<AboutViewModel>();
// 建立 Provider
Services = collection.BuildServiceProvider();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// 解析 MainWindow 並注入 ViewModel
var mainVm = Services.GetRequiredService<MainWindowViewModel>();
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow = new MainWindow
{
DataContext = mainVm
};
}
base.OnFrameworkInitializationCompleted();
}
...略
側邊欄導航邏輯實作 (Navigation Logic)
為了實現側邊欄的頁面切換與資源管理,我們需要對 MainWindowViewModel 進行重構,主要分為「依賴注入設定」與「導航邏輯實作」兩個部分。 在 MainWindowViewModel 中,我們實作了側邊欄的導航邏輯。主要目標是:當使用者選擇選單項目時,能夠切換顯示對應的 ViewModel,同時確保頁面狀態不會因為切換而消失(例如在設定頁輸入的內容切換回來後還在)。
1. 基礎設置:依賴注入與繼承
首先,修改 MainWindowViewModel,改用 C# 12 的 Primary Constructor (主要建構函數) 語法,並透過建構式注入 (Constructor Injection) 傳入 IServiceProvider 以便後續取得服務。 同時,別忘了繼承 ViewModelBase 以確保 MVVM 功能運作正常:
C#
public partial class MainWindowViewModel(IServiceProvider services) : ViewModelBase
{
// ... 內部的程式碼
}
有了這個 services 變數後,我們就能在接下來的邏輯中,動態向系統請求所需的 ViewModel 實例。
2. 頁面快取與選單定義
首先,我們宣告了 _homeViewModel、_settingsViewModel 等私有欄位,這些變數用於 快取 (Cache) 已經實例化的頁面。這樣當使用者在頁面間來回切換時,我們不需要每次都重新建立新的 ViewModel,從而保留頁面操作狀態並節省效能。 同時,定義 MenuItems 列表(包含 "首頁"、"設定"、"關於"),供前端 View 的 ListBox 綁定顯示。
3. 狀態綁定 (Observable Properties)
我們定義了兩個關鍵的可觀察屬性:
SelectedMenuItem:綁定到側邊欄選單的選擇項,用來追蹤使用者點了哪個按鈕。CurrentPage:這是導航的核心,它代表目前右側主畫面要顯示的內容。初始值我們透過services.GetRequiredService<HomeViewModel>()直接載入首頁,確保程式一啟動就有畫面。
4. 觸發導航 (Command & Interaction)
透過 [RelayCommand] 建立 OnMenuSelected 函數。當 View 端的選單選項改變時(透過綁定觸發),此函數會執行:
- 呼叫
Navigate()進行頁面切換。 - 判斷
IsSidebarOpen,如果在開啟狀態下點擊了選項,則自動關閉側邊欄,提供更流暢的使用者體驗 (UX)。
5. 導航方法 (Navigate with Lazy Loading)
Navigate 方法使用了 C# 的 switch 表達式搭配 空值合併指派運算子 (??=) 來實現 懶加載 (Lazy Loading) 機制:
C#
"首頁" => _homeViewModel ??= services.GetRequiredService<HomeViewModel>()
這段語法的含義是:「檢查 _homeViewModel 是否已經有值?如果有,直接使用它(快取);如果沒有(即第一次開啟該頁面),則向 DI 容器請求一個新的實例並存入變數中。」
透過這種方式,我們實現了按需載入(On-Demand Loading),既優化了記憶體使用,又確保了頁面狀態的連續性。
C#
MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
namespace AvaloniaSideBarEx.ViewModels;
public partial class MainWindowViewModel(IServiceProvider services) : ViewModelBase
{
//側邊欄控制
[ObservableProperty]
private bool _isSidebarOpen;
[RelayCommand]
private void ToggleSidebar()
{
IsSidebarOpen = !IsSidebarOpen;
}
//側邊欄導航
private HomeViewModel? _homeViewModel;
private SettingsViewModel? _settingsViewModel;
private AboutViewModel? _aboutViewModel;
public List<string> MenuItems { get; } = ["首頁", "設定", "關於"];
[ObservableProperty]
private string? _selectedMenuItem = "首頁";
[ObservableProperty]
private ObservableObject? _currentPage = services.GetRequiredService<HomeViewModel>();
// 這是 CommunityToolkit 自動生成的鉤子方法 (Hook)
// 當 _selectedMenuItem 改變時,這個函數會被自動呼叫
partial void OnSelectedMenuItemChanged(string? value)
{
// 直接在這裡呼叫你的導航邏輯
OnMenuSelected();
}
[RelayCommand]
private void OnMenuSelected()
{
//Debug.WriteLine($"當前選中:{SelectedMenuItem}");
Navigate(SelectedMenuItem);
if (IsSidebarOpen)
{
IsSidebarOpen = false;
}
}
private void Navigate(string? target)
{
//根據選中的菜單名稱,實例化對應的 ViewModel
CurrentPage = target switch
{
"首頁" => _homeViewModel ??= services.GetRequiredService<HomeViewModel>(), // 如果 _homeViewModel 是 null,則創建新的並賦值
"設定" => _settingsViewModel ??= services.GetRequiredService<SettingsViewModel>(),
"關於" => _aboutViewModel ??= services.GetRequiredService<AboutViewModel>(),
_ => _homeViewModel ??= services.GetRequiredService<HomeViewModel>()
};
}
}
xml
MainWindow.axml
<SplitView.Pane>
<Border BorderBrush="#BDC3C7"
BorderThickness="0, 0, 1, 0">
<StackPanel Background="#ECF0F1">
<TextBlock Text="導航選單"
FontSize="16"
HorizontalAlignment="Center"
Margin="15, 20, 15, 10"/>
<ListBox Background="Transparent"
BorderThickness="0"
Padding="0"
SelectionMode="Single"
ItemsSource="{Binding MenuItems}"
SelectedItem="{Binding SelectedMenuItem, Mode=TwoWay}"
SelectedIndex="0">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding }"
Margin="3"
HorizontalAlignment="Center"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Border>
</SplitView.Pane>
<SplitView.Content>
<Grid RowDefinitions="*, Auto" ColumnDefinitions="*, *, *" Background="#ECF0F1">
<ContentControl Grid.Row="0"
Grid.ColumnSpan="3"
Content="{Binding CurrentPage}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"/>
<Border Grid.Column="0"
Grid.Row="1"
Grid.ColumnSpan="3"
BorderBrush="#BDC3C7"
BorderThickness="0, 1, 0, 0"
Padding="10">
<TextBlock Text="Xian - 版權所有 © 2025"HorizontalAlignment="Right"
FontSize="12"/>
</Border>
</Grid>
</SplitView.Content>